#define _GNU_SOURCE

#include <stdlib.h>
#include <assert.h>
#include <ctype.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <string.h>

#include "http.h"


char* urlencode(char* buf, size_t blen, const char* s)
{
    int i, j = 0;
    for (i=0; s[i]!='\0' && j+1<blen; ++i) {
        if ((s[i]>='A'&&s[i]<='Z') ||
            (s[i]>='a'&&s[i]<='z') ||
            (s[i]>='0'&&s[i]<='9') ||
            NULL!=strchr("-_.!~*'();/?:@&=+$,#", s[i]))
            buf[j++] = s[i];
        else {
            if (j+4<=blen) {
                snprintf(buf+j, 4, "%%%02X", (unsigned char)s[i]);
                j += 3;
            }
            else
                break;
        }
    }
    buf[j] = '\0';
    return buf;    
}

/* url encode */
char* urlencode_all(char* buf, size_t blen, const char* s)
{
    int i, j = 0;
    for (i=0; s[i]!='\0' && j+1<blen; ++i) {
        if ((s[i]>='A'&&s[i]<='Z') ||
            (s[i]>='a'&&s[i]<='z') ||
            (s[i]>='0'&&s[i]<='9') ||
            NULL!=strchr("-_.", s[i]))
            buf[j++] = s[i];
        else {
            if (j+4<=blen) {
                snprintf(buf+j, 4, "%%%02X", (unsigned char)s[i]);
                j += 3;
            }
            else
                break;
        }
    }
    buf[j] = '\0';
    return buf;
}

/* hex char(eg. 'e','f') to int. */
static int chex2int(char h)
{
    if (h>='0' && h<='9') return h-'0';
    if (h>='a' && h<='f') return h-'a'+10;
    if (h>='A' && h<='F') return h-'A'+10;
    return 0;
}

/* url decode */
char* urldecode(char* buf, size_t blen, const char* s)
{
    int i, j = 0;
    for (i=0; i+1<blen && s[j]!='\0'; ++i) {
        if (s[j]=='%') {
            if (s[j+1]!='\0' && s[j+2]!='\0') {
                char hex[] = { s[j+1], s[j+2] };
                buf[i] = chex2int(hex[0])*16+chex2int(hex[1]);
                j += 3;
                continue;
            }
        }
        buf[i] = s[j++];
    }
    buf[i] = '\0';
    return buf;
}

/* shift a data from http->data */
static char* qdata(HTTP* http)
{
    if (http==NULL || http->data==NULL) return NULL;

    struct strgarbage* g = http->garbage;
    http->garbage = malloc(sizeof(struct strgarbage));
    http->garbage->next = g;
    http->garbage->str = http->data->str;
    struct hnode* p = http->data;
    http->data = p->next;
    free(p);
    return http->garbage->str;
}

/* shift a header from the head of the header-queue */
static char* qheader(HTTP* http)
{
    if (http==NULL || http->head==NULL) return NULL;
    
    struct strgarbage* g = http->garbage;
    http->garbage = malloc(sizeof(struct strgarbage));
    http->garbage->next = g;
    http->garbage->str = http->head->str;
    struct hnode* p = http->head;
    http->head = p->next;
    free(p);
    return http->garbage->str;
}

/* append a header into header-queue */
static void qpush(HTTP* http, const char* str)
{
    if (http==NULL) return;
    
    struct hnode* p = malloc(sizeof(struct hnode));
    p->next = NULL;
    size_t len = strlen(str);
    p->str = malloc(len + 3);
    memcpy(p->str, str, len);
    memcpy(p->str+len, "\r\n", 3);
    if (http->tail==NULL)
        http->head = http->tail = p;
    else {
        http->tail->next = p;
        http->tail = p;
    }
}

/* recursive find str in header queue */
static struct hnode* qrf(struct hnode* p, const char* str, int n)
{
    if (p==NULL) return NULL;
    /* case-insensitive string cmp */
    int i;
    for (i=0; i<n; ++i)
        if (tolower(p->str[i])!=tolower(str[i]))
            break;
    if (i==n) return p;
    return qrf(p->next, str, n);
}

/* find a similar header node */
static struct hnode* qfind(HTTP* http, const char* str)
{
    assert(http);
    /* find header name */
    int n = strchr(str, ':') - str;
    if (n<=0) return NULL;
    return qrf(http->head, str, n);
}

/* open a HTTP request pointer */
HTTP* http_open(int sck)
{
    HTTP* http = malloc(sizeof(HTTP));
    if (http==NULL) return NULL;

    http->sck     = sck;
    http->tail    = http->head = NULL;
    http->data    = NULL;
    http->garbage = NULL;

    /* set default header, user can use http_header(..., 1) to replace
       the defaults. */
    http_header(http, "Accept: */*", 0);
    http_header(http, "Pragma: no-cache", 0);
    http_header(http, "Cache-Control: no-cache", 0);
    
    return http;
}

/* close a HTTP request pointer. */
void http_close(HTTP* http)
{
    if (http==NULL) return;

    /* release string garbage */
    struct strgarbage* g;
    while ((g = http->garbage) != NULL) {
        free(g->str);
        http->garbage = g->next;
        free(g);
    }

    /* release unused headers */
    struct hnode* p;
    while ((p = http->head) != NULL) {
        free(p->str);
        http->head = p->next;
        free(p);
    }

    /* release unused data */
    while ((p = http->data) != NULL) {
        free(p->str);
        http->data = p->next;
        free(p);
    }

    free(http);
    http = 0;
}

/* add a header */
void http_header(HTTP* http, const char* str, int replace)
{
    if (replace) {
        struct hnode* p = qfind(http, str);
        if (p!=NULL) { /* found */
            free(p->str);
            size_t len = strlen(str);
            p->str = malloc(len+3);
            memcpy(p->str, str, len);
            memcpy(p->str+len, "\r\n", 3);
            return;
        }
    }
    
    qpush(http, str); /* not found or replace==0 */
}

/* set http request data. */
void http_data(HTTP* http, const char* name, const char* value)
{
    /* encode name and value */
    size_t len = (strlen(name)+strlen(value))*3 + 2;
    char* buf = malloc(len);
    size_t n1 = strlen(urlencode_all(buf, len, name));
    buf[n1] = '=';
    size_t n2 = strlen(urlencode_all(buf+n1+1, len-1-n1, value));

    struct hnode* p = malloc(sizeof(struct hnode));
    p->str = realloc(buf, n1+n2+2); /* re-alloc to real length. I guess it will
                                       always success. */
    p->next = http->data;
    http->data = p;
}

/* Send a whole string buffer to sck, and the string is null terminated.
 * NOTICE: This function is based on writen() in "Unix Network
 *         Programming" with slight changing.
 *
 * return: on error, returns -1. otherwise return the number of bytes
 *         successfully sent.
 */
static int sendstr(int sck, const char* str, FILE* echo)
{
    if (echo) fprintf(echo, "%s",str);

    size_t n = strlen(str);
    size_t left = n;
    size_t m;
    const char* p = str;
    while (left>0) {
        if ((m=send(sck, p, left, 0)) <= 0) {
            if (m<0 && errno==EINTR)
                m = 0;   /* and call send() again */
            else
                return -1;   /* error */
        }
        left -= m;
        p += m;
    }
    return n;
}

/* recieve a whole line, and return recieved bytes count. */
static int recvln(int sck, char* buf, size_t blen)
{
    int n, m;
    char* p = buf;
    for (n=1; n<blen; ++n, ++p) {
    AGAIN:
        if ((m=recv(sck, p, 1, 0)) == 1) {
            if (*p=='\n')
                break; /* get to the end of this line. */
        }
        else if (m==0) { /* EOF, host closes sending. */
            *p = 0;
            return n-1;
        }
        else {
            if (errno==EINTR)
                goto AGAIN;
            return -1; /* on error */
        }
    }
    
    ++p;
    *p = '\0';
    return n;
}

/* perform HTTP GET method to retrieve data */
int http_get(HTTP* http, httpopt_t* hopt, char* buf, size_t blen)
{
    char encode[10240]; /* 10k encode buffer */
    
    /* send request start line */
    sendstr(http->sck, "GET ", hopt->request);
    sendstr(http->sck, urlencode(encode, 10240, hopt->path), hopt->request);
    sendstr(http->sck, " HTTP/1.1\r\nHost: ", hopt->request);
    sendstr(http->sck, hopt->host, hopt->request);
    sendstr(http->sck, ":", hopt->request);
    char port[21];
    snprintf(port, 20, "%u\r\n", hopt->port);
    if (sendstr(http->sck, port, hopt->request)==-1)
        return -1; /* fail to send start line */

    /* send headers: */
    char* s;
    while ((s = qheader(http)) != NULL) {
        if (sendstr(http->sck, s, hopt->request) == -1)
            return -1;
    }

    /* send data */
    sendstr(http->sck, "\r\n", hopt->request); /* a blank line */
    char sep = 0;
    while ((s = qdata(http)) != NULL) {
        if (sep) {
            sendstr(http->sck, "&", hopt->request);
        }
        else
            sep = 1;
        sendstr(http->sck, s, hopt->request);
    }
    sendstr(http->sck, "\r\n", hopt->request);

    /* wait for response header */
    int n = 0;
    int total = 0;
    int m;
    while (buf!=NULL && blen>n && (m=recvln(http->sck, buf+n, blen-n))>0) {
        if (m==2 && buf[n]=='\r' && buf[n+1]=='\n') {
            n += 2;
            buf[n] = '\0';
            return total;
        }
        else {
            if (strncasecmp("Content-Length", buf+n, 14)==0)
                total = atoi(buf+n+15);
            n += m;
        }
    }
    if (buf!=NULL)
        buf[blen-1] = '\0';
    char padding[3];
    while (m>0 && (m=recvln(http->sck, padding, 3))>0) {
        if (m==2 && padding[0]=='\r' && padding[1]=='\n')
            break;
    }

    /* ignore recv errors */
    return total;
}

/* read http data */
int http_read(HTTP* http, char* buf, size_t size)
{
    int len;
AGAIN:
    if ((len=recv(http->sck, buf, size, 0))<0) {
        if (errno==EINTR)
            goto AGAIN;
        else
            return -1;
    }
    return len;
}

/* get 'Set-Cookie' field in header. */
char* http_get_cookie(const char* header)
{
    return strcasestr(header, "set-cookie:");
}
